/**
 * Copyright 2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xume.raob.core;

import static org.xume.raob.util.ReflectionUtils.createInstance;
import static org.xume.raob.util.ReflectionUtils.isBuildMethod;
import static org.xume.raob.util.ReflectionUtils.targetClass;

import java.lang.annotation.Annotation;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.xume.raob.api.BuildStepFactory;
import org.xume.raob.api.BuildStepFactoryClass;
import org.xume.raob.api.ObjectBuilder;
import org.xume.raob.api.ObjectBuilderConfigurationException;

/**
 * @author Johan Siebens
 */
@SuppressWarnings("unchecked")
public final class IntrospectionResult {

	private static final Map<Class<?>, IntrospectionResult> CACHE = new ConcurrentHashMap<Class<?>, IntrospectionResult>();

	public static IntrospectionResult forBuilder(Class<? extends ObjectBuilder<?>> builderInterface) {
		IntrospectionResult result = CACHE.get(builderInterface);
		if (result == null) {
			result = new IntrospectionResult(targetClass(builderInterface));

			for (Method method : builderInterface.getMethods()) {
				if (!isBuildMethod(method)) {
					result.parseBuilderMethod(builderInterface, method);
				}

			}

			CACHE.put(builderInterface, result);
		}
		return result;
	}

	private static boolean hasValidReturnType(Class<?> builderInterface, Method method) {
		Type type = method.getGenericReturnType();
		if (type instanceof Class<?>) {
			return ((Class<?>) type).isAssignableFrom(builderInterface);
		} else if (type instanceof TypeVariable<?>) {
			TypeVariable<?> typeVariable = (TypeVariable<?>) type;
			GenericDeclaration gd = typeVariable.getGenericDeclaration();
			if (gd instanceof Class<?>) {
				return ((Class<?>) gd).isAssignableFrom(builderInterface);
			}
		}
		return false;
	}

	private static boolean isBuildStepAnnotation(Annotation annotation) {
		return lookupBuildStepFactoryClass(annotation) != null;
	}

	private static BuildStepFactoryClass lookupBuildStepFactoryClass(Annotation annotation) {
		Class<? extends Annotation> annotationType = annotation.annotationType();
		return annotationType.getAnnotation(BuildStepFactoryClass.class);
	}

	private Map<Method, BuildStepFactory<?>> buildStepFactories;

	private Class<?> clazz;

	private IntrospectionResult(Class<?> targetClass) {
		this.clazz = targetClass;
		this.buildStepFactories = new HashMap<Method, BuildStepFactory<?>>();
	}

	public BuildStepFactory<?> getBuildStepFactory(Method method) {
		return this.buildStepFactories.get(method);
	}

	public Class<?> getTargetClass() {
		return clazz;
	}

	public boolean isBuildStep(Method method) {
		return this.buildStepFactories.containsKey(method);
	}

	private void parseBuilderMethod(Class<? extends ObjectBuilder<?>> builderInterface, Method method) {
		if (hasValidReturnType(builderInterface, method)) {
			Annotation[] annotations = method.getAnnotations();
			List<Annotation> buildStepAnnotations = new ArrayList<Annotation>();

			for (Annotation annotation : annotations) {
				if (isBuildStepAnnotation(annotation)) {
					buildStepAnnotations.add(annotation);
				}
			}

			switch (buildStepAnnotations.size()) {
			case 1:
				Annotation buildStepAnnotation = buildStepAnnotations.get(0);
				BuildStepFactoryClass buildStepFactoryClass = lookupBuildStepFactoryClass(buildStepAnnotation);
				BuildStepFactory buildStepFactory = createInstance(buildStepFactoryClass.value());
				buildStepFactory.initialize(buildStepAnnotation, method, clazz);
				buildStepFactories.put(method, buildStepFactory);
				break;
			case 0:
				throw new ObjectBuilderConfigurationException("Method [" + method.getName() + "] declared in ["
						+ builderInterface.getSimpleName() + "] is missing a BuildStep Annotation");
			default:
				throw new ObjectBuilderConfigurationException("Method [" + method.getName() + "] declared in ["
						+ builderInterface.getSimpleName() + "] has multiple BuildStep Annotations");
			}

		} else {
			throw new ObjectBuilderConfigurationException("Methods declared in [" + builderInterface.getSimpleName()
					+ "] should return an instance of [" + builderInterface.getSimpleName() + "]");
		}
	}

}
